页面权限控制&菜单权限
概述
本节介绍前端权限控制体系,包括按钮级权限(指令)、页面级权限(路由守卫)和菜单级权限(动态菜单),以及前后端协作的安全方案。
前端权限控制三层体系
┌─────────────────────────────────┐
│ 菜单权限:控制用户可见的菜单项 │ ← 本节重点
├─────────────────────────────────┤
│ 页面权限:控制用户可访问的路由 │ ← 本节重点
├─────────────────────────────────┤
│ 按钮权限:控制页面内的操作按钮 │ ← v-hasPermission 指令
├─────────────────────────────────┤
│ 接口权限:后端验证请求合法性 │ ← 服务端职责
└─────────────────────────────────┘
text
路由权限控制
公共路由 vs 私有路由
// router/index.ts
import type { RouteRecordRaw } from 'vue-router'
// 公共路由:无需登录即可访问
const publicRoutes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: { public: true }
},
{
path: '/404',
name: 'NotFound',
component: () => import('@/views/error/404.vue'),
meta: { public: true }
}
]
// 私有路由:需要权限控制
const privateRoutes: RouteRecordRaw[] = [
{
path: '/admin',
component: () => import('@/layouts/default.vue'),
meta: { requiresAuth: true },
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: { title: '首页', icon: 'Home' }
},
{
path: 'users',
name: 'UserManagement',
component: () => import('@/views/system/users.vue'),
meta: {
title: '用户管理',
icon: 'User',
permission: 'system:user:list'
}
}
]
}
]
typescript
路由守卫实现
// router/guard.ts
import type { Router } from 'vue-router'
export function setupPermissionGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
// 公共路由直接放行
if (to.meta.public) {
next()
return
}
const token = localStorage.getItem('token')
// 未登录 → 跳转登录页
if (!token) {
next({ path: '/login', query: { redirect: to.fullPath } })
return
}
// 检查页面权限
const requiredPermission = to.meta.permission as string | undefined
if (requiredPermission) {
const userPermissions = getUserPermissions()
if (!userPermissions.includes(requiredPermission)) {
// 无权限 → 跳转到默认页
next({ path: '/admin/dashboard' })
return
}
}
next()
})
}
typescript
菜单权限控制
动态菜单生成
// stores/menu.ts
import { defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'
interface MenuItem {
path: string
title: string
icon?: string
children?: MenuItem[]
}
export const useMenuStore = defineStore('menu', () => {
const menuList = ref<MenuItem[]>([])
const permissions = ref<string[]>([])
// 根据权限过滤路由,生成菜单
function generateMenus(routes: RouteRecordRaw[], perms: string[]): MenuItem[] {
permissions.value = perms
return routes
.filter(route => {
const perm = route.meta?.permission as string | undefined
// 无权限要求 或 用户拥有该权限
return !perm || perms.includes(perm)
})
.map(route => ({
path: route.path,
title: route.meta?.title as string || '',
icon: route.meta?.icon as string || '',
children: route.children
? generateMenus(route.children, perms)
: undefined
}))
.filter(item => item.title) // 过滤掉无标题的路由
}
async function fetchUserMenus() {
// 从后端获取用户权限列表
const userPerms = await api.getUserPermissions()
const menus = generateMenus(privateRoutes, userPerms)
menuList.value = menus
}
return { menuList, permissions, generateMenus, fetchUserMenus }
})
typescript
菜单组件中使用
<template>
<el-menu :default-active="activeMenu">
<template v-for="item in menuList" :key="item.path">
<!-- 有子菜单 -->
<el-sub-menu v-if="item.children?.length" :index="item.path">
<template #title>
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.title }}</span>
</template>
<el-menu-item
v-for="child in item.children"
:key="child.path"
:index="child.path"
@click="router.push(child.path)"
>
{{ child.title }}
</el-menu-item>
</el-sub-menu>
<!-- 无子菜单 -->
<el-menu-item v-else :index="item.path" @click="router.push(item.path)">
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.title }}</span>
</el-menu-item>
</template>
</el-menu>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useMenuStore } from '@/stores/menu'
const route = useRoute()
const router = useRouter()
const menuStore = useMenuStore()
const menuList = computed(() => menuStore.menuList)
const activeMenu = computed(() => route.path)
</script>
vue
安全性分析
| 层级 | 攻击向量 | 防御措施 |
|---|---|---|
| 前端菜单 | 用户修改 localStorage | 菜单仅影响可见性,不决定数据安全 |
| 前端路由 | 用户直接输入 URL | 路由守卫拦截,但可绕过 |
| 后端接口 | 用户发起越权请求 | 关键防御层:接口权限校验 |
核心原则:前端权限控制只影响用户体验(隐藏不可见的菜单和按钮),真正的安全保障依赖后端接口权限校验。即使前端被绕过,后端仍能拦截非法请求。
小结
- 前端权限体系分三层:菜单权限、页面权限、按钮权限
- 路由分为公共路由(写死)和私有路由(根据权限动态生成)
- 使用路由守卫拦截未授权的页面访问
- 菜单权限根据用户权限列表动态过滤路由配置
- 前端权限只是 UX 层面的控制,接口权限才是安全的核心保障
↑